<!doctype html>
<meta charset=utf-8>
<script src="../testcommon.js"></script>
<style>
@keyframes anim1 {
  to { left: 100px }
}
@keyframes anim2 {
  to { top: 100px }
}
@keyframes multiPropAnim {
  to { background: green, opacity: 0.5, left: 100px, top: 100px }
}
@keyframes empty { }
</style>
<body>
<script>
'use strict';

test(function(t) {
  var div = addDiv(t);
  assert_equals(div.getAnimations().length, 0,
    'getAnimations returns an empty sequence for an element'
    + ' with no animations');
}, 'getAnimations for non-animated content');

promise_test(function(t) {
  var div = addDiv(t);

  // FIXME: This test does too many things. It should be split up.

  // Add an animation
  div.style.animation = 'anim1 100s';
  var animations = div.getAnimations();
  assert_equals(animations.length, 1,
    'getAnimations returns an Animation running CSS Animations');
  return animations[0].ready.then(function() {
    var startTime = animations[0].startTime;
    assert_true(startTime > 0 && startTime <= document.timeline.currentTime,
      'CSS animation has a sensible start time');

    // Wait a moment then add a second animation.
    //
    // We wait for the next frame so that we can test that the start times of
    // the animations differ.
    return waitForFrame();
  }).then(function() {
    div.style.animation = 'anim1 100s, anim2 100s';
    animations = div.getAnimations();
    assert_equals(animations.length, 2,
      'getAnimations returns one Animation for each value of'
      + ' animation-name');
    // Wait until both Animations are ready
    // (We don't make any assumptions about the order of the Animations since
    //  that is the purpose of the following test.)
    return waitForAllAnimations(animations);
  }).then(function() {
    assert_true(animations[0].startTime < animations[1].startTime,
      'Additional Animations for CSS animations start after the original'
      + ' animation and appear later in the list');
  });
}, 'getAnimations for CSS Animations');

test(function(t) {
  var div = addDiv(t, { style: 'animation: anim1 100s' });
  assert_class_string(div.getAnimations()[0], 'CSSAnimation',
                      'Interface of returned animation is CSSAnimation');
}, 'getAnimations returns CSSAnimation objects for CSS Animations');

test(function(t) {
  var div = addDiv(t);

  // Add an animation that targets multiple properties
  div.style.animation = 'multiPropAnim 100s';
  assert_equals(div.getAnimations().length, 1,
    'getAnimations returns only one Animation for a CSS Animation'
    + ' that targets multiple properties');
}, 'getAnimations for multi-property animations');

promise_test(function(t) {
  var div = addDiv(t);

  // Add an animation
  div.style.backgroundColor = 'red';
  div.style.animation = 'anim1 100s';
  window.getComputedStyle(div).backgroundColor;

  // Wait until a frame after the animation starts, then add a transition
  var animations = div.getAnimations();
  return animations[0].ready.then(waitForFrame).then(function() {
    div.style.transition = 'all 100s';
    div.style.backgroundColor = 'green';

    animations = div.getAnimations();
    assert_equals(animations.length, 2,
      'getAnimations returns Animations for both animations and'
      + ' transitions that run simultaneously');
    assert_class_string(animations[0], 'CSSTransition',
                        'First-returned animation is the CSS Transition');
    assert_class_string(animations[1], 'CSSAnimation',
                        'Second-returned animation is the CSS Animation');
  });
}, 'getAnimations for both CSS Animations and CSS Transitions at once');

async_test(function(t) {
  var div = addDiv(t);

  // Set up event listener
  div.addEventListener('animationend', t.step_func(function() {
    assert_equals(div.getAnimations().length, 0,
      'getAnimations does not return Animations for finished '
      + ' (and non-forwards-filling) CSS Animations');
    t.done();
  }));

  // Add a very short animation
  div.style.animation = 'anim1 0.01s';
}, 'getAnimations for CSS Animations that have finished');

async_test(function(t) {
  var div = addDiv(t);

  // Set up event listener
  div.addEventListener('animationend', t.step_func(function() {
    assert_equals(div.getAnimations().length, 1,
      'getAnimations returns Animations for CSS Animations that have'
      + ' finished but are filling forwards');
    t.done();
  }));

  // Add a very short animation
  div.style.animation = 'anim1 0.01s forwards';
}, 'getAnimations for CSS Animations that have finished but are'
   + ' forwards filling');

test(function(t) {
  var div = addDiv(t);
  div.style.animation = 'none 100s';

  var animations = div.getAnimations();
  assert_equals(animations.length, 0,
    'getAnimations returns an empty sequence for an element'
    + ' with animation-name: none');

  div.style.animation = 'none 100s, anim1 100s';
  animations = div.getAnimations();
  assert_equals(animations.length, 1,
    'getAnimations returns Animations only for those CSS Animations whose'
    + ' animation-name is not none');
}, 'getAnimations for CSS Animations with animation-name: none');

test(function(t) {
  var div = addDiv(t);
  div.style.animation = 'missing 100s';
  var animations = div.getAnimations();
  assert_equals(animations.length, 0,
    'getAnimations returns an empty sequence for an element'
    + ' with animation-name: missing');

  div.style.animation = 'anim1 100s, missing 100s';
  animations = div.getAnimations();
  assert_equals(animations.length, 1,
    'getAnimations returns Animations only for those CSS Animations whose'
    + ' animation-name is found');
}, 'getAnimations for CSS Animations with animation-name: missing');

promise_test(function(t) {
  var div = addDiv(t);
  div.style.animation = 'anim1 100s, notyet 100s';
  var animations = div.getAnimations();
  assert_equals(animations.length, 1,
    'getAnimations initally only returns Animations for CSS Animations whose'
    + ' animation-name is found');

  return animations[0].ready.then(waitForFrame).then(function() {
    var keyframes = '@keyframes notyet { to { left: 100px; } }';
    document.styleSheets[0].insertRule(keyframes, 0);
    animations = div.getAnimations();
    assert_equals(animations.length, 2,
      'getAnimations includes Animation when @keyframes rule is added'
      + ' later');
    return waitForAllAnimations(animations);
  }).then(function() {
    assert_true(animations[0].startTime < animations[1].startTime,
      'Newly added animation has a later start time');
    document.styleSheets[0].deleteRule(0);
  });
}, 'getAnimations for CSS Animations where the @keyframes rule is added'
   + ' later');

test(function(t) {
  var div = addDiv(t);
  div.style.animation = 'anim1 100s, anim1 100s';
  assert_equals(div.getAnimations().length, 2,
    'getAnimations returns one Animation for each CSS animation-name'
    + ' even if the names are duplicated');
}, 'getAnimations for CSS Animations with duplicated animation-name');

test(function(t) {
  var div = addDiv(t);
  div.style.animation = 'empty 100s';
  assert_equals(div.getAnimations().length, 1,
    'getAnimations returns Animations for CSS animations with an'
    + ' empty keyframes rule');
}, 'getAnimations for CSS Animations with empty keyframes rule');

promise_test(function(t) {
  var div = addDiv(t);
  div.style.animation = 'anim1 100s 100s';
  var animations = div.getAnimations();
  assert_equals(animations.length, 1,
    'getAnimations returns animations for CSS animations whose'
    + ' delay makes them start later');
  return animations[0].ready.then(waitForFrame).then(function() {
    assert_true(animations[0].startTime <= document.timeline.currentTime,
      'For CSS Animations in delay phase, the start time of the Animation is'
      + ' not in the future');
  });
}, 'getAnimations for CSS animations in delay phase');

test(function(t) {
  var div = addDiv(t);
  div.style.animation = 'anim1 0s 100s';
  assert_equals(div.getAnimations().length, 1,
    'getAnimations returns animations for CSS animations whose'
    + ' duration is zero');
  div.remove();
}, 'getAnimations for zero-duration CSS Animations');

test(function(t) {
  var div = addDiv(t);
  div.style.animation = 'anim1 100s';
  var originalAnimation = div.getAnimations()[0];

  // Update pause state (an Animation change)
  div.style.animationPlayState = 'paused';
  var pendingAnimation = div.getAnimations()[0];
  assert_equals(pendingAnimation.playState, 'pending',
                'animation\'s play state is updated');
  assert_equals(originalAnimation, pendingAnimation,
                'getAnimations returns the same objects even when their'
                + ' play state changes');

  // Update duration (an Animation change)
  div.style.animationDuration = '200s';
  var extendedAnimation = div.getAnimations()[0];
  // FIXME: Check extendedAnimation.effect.timing.duration has changed once the
  // API is available
  assert_equals(originalAnimation, extendedAnimation,
                'getAnimations returns the same objects even when their'
                + ' duration changes');
}, 'getAnimations returns objects with the same identity');

test(function(t) {
  var div = addDiv(t);
  div.style.animation = 'anim1 100s';

  assert_equals(div.getAnimations().length, 1,
    'getAnimations returns an animation before cancelling');

  var animation = div.getAnimations()[0];

  animation.cancel();
  assert_equals(div.getAnimations().length, 0,
    'getAnimations does not return cancelled animations');

  animation.play();
  assert_equals(div.getAnimations().length, 1,
    'getAnimations returns cancelled animations that have been re-started');

}, 'getAnimations for CSS Animations that are cancelled');

promise_test(function(t) {
  var div = addDiv(t);
  div.style.animation = 'anim2 100s';

  return div.getAnimations()[0].ready.then(function() {
    // Prepend to the list and test that even though anim1 was triggered
    // *after* anim2, it should come first because it appears first
    // in the animation-name property.
    div.style.animation = 'anim1 100s, anim2 100s';
    var anims = div.getAnimations();
    assert_equals(anims[0].animationName, 'anim1',
                  'animation order after prepending to list');
    assert_equals(anims[1].animationName, 'anim2',
                  'animation order after prepending to list');

    // Normally calling cancel and play would this push anim1 to the top of
    // the stack but it shouldn't for CSS animations that map an the
    // animation-name property.
    var anim1 = anims[0];
    anim1.cancel();
    anim1.play();
    anims = div.getAnimations();
    assert_equals(anims[0].animationName, 'anim1',
                  'animation order after cancelling and restarting');
    assert_equals(anims[1].animationName, 'anim2',
                  'animation order after cancelling and restarting');
  });
}, 'getAnimations for CSS Animations follows animation-name order');

test(function(t) {
  addStyle(t, { '#target::after': 'animation: anim1 10s;',
                '#target::before': 'animation: anim1 10s;' });
  var target = addDiv(t, { 'id': 'target' });
  target.style.animation = 'anim1 100s';

  var animations = target.getAnimations({ subtree: false });
  assert_equals(animations.length, 1,
                'Should find only the element');
  assert_equals(animations[0].effect.target, target,
                'Effect target should be the element');
}, 'Test AnimationFilter{ subtree: false } with single element');

test(function(t) {
  addStyle(t, { '#target::after': 'animation: anim1 10s;',
                '#target::before': 'animation: anim1 10s;' });
  var target = addDiv(t, { 'id': 'target' });
  target.style.animation = 'anim1 100s';

  var animations = target.getAnimations({ subtree: true });
  assert_equals(animations.length, 3,
                'getAnimations({ subtree: true }) ' +
                'should return animations on pseudo-elements');
  assert_equals(animations[0].effect.target, target,
                'The animation targeting the parent element ' +
                'should be returned first');
  assert_equals(animations[1].effect.target.type, '::before',
                'The animation targeting the ::before pseudo-element ' +
                'should be returned second');
  assert_equals(animations[2].effect.target.type, '::after',
                'The animation targeting the ::after pesudo-element ' +
                'should be returned last');
}, 'Test AnimationFilter{ subtree: true } with single element');

test(function(t) {
  addStyle(t, { '#parent::after': 'animation: anim1 10s;',
                '#parent::before': 'animation: anim1 10s;',
                '#child::after': 'animation: anim1 10s;',
                '#child::before': 'animation: anim1 10s;' });
  var parent = addDiv(t, { 'id': 'parent' });
  parent.style.animation = 'anim1 100s';
  var child = addDiv(t, { 'id': 'child' });
  child.style.animation = 'anim1 100s';
  parent.appendChild(child);

  var animations = parent.getAnimations({ subtree: false });
  assert_equals(animations.length, 1,
                'Should find only the element even if it has a child');
  assert_equals(animations[0].effect.target, parent,
                'Effect target shuld be the element');
}, 'Test AnimationFilter{ subtree: false } with element that has a child');

test(function(t) {
  addStyle(t, { '#parent::after': 'animation: anim1 10s;',
                '#parent::before': 'animation: anim1 10s;',
                '#child::after': 'animation: anim1 10s;',
                '#child::before': 'animation: anim1 10s;' });
  var parent = addDiv(t, { 'id': 'parent' });
  var child = addDiv(t, { 'id': 'child' });
  parent.style.animation = 'anim1 100s';
  child.style.animation = 'anim1 100s';
  parent.appendChild(child);

  var animations = parent.getAnimations({ subtree: true });
  assert_equals(animations.length, 6,
                'Should find all elements, pesudo-elements that parent has');

  assert_equals(animations[0].effect.target, parent,
                'The animation targeting the parent element ' +
                'should be returned first');
  assert_equals(animations[1].effect.target.type, '::before',
                'The animation targeting the ::before pseudo-element ' +
                'should be returned second');
  assert_equals(animations[1].effect.target.parentElement, parent,
                'This ::before element should be child of parent element');
  assert_equals(animations[2].effect.target.type, '::after',
                'The animation targeting the ::after pesudo-element ' +
                'should be returned third');
  assert_equals(animations[2].effect.target.parentElement, parent,
                'This ::after element should be child of parent element');

  assert_equals(animations[3].effect.target, child,
                'The animation targeting the child element ' +
                'should be returned fourth');
  assert_equals(animations[4].effect.target.type, '::before',
                'The animation targeting the ::before pseudo-element ' +
                'should be returned fifth');
  assert_equals(animations[4].effect.target.parentElement, child,
                'This ::before element should be child of child element');
  assert_equals(animations[5].effect.target.type, '::after',
                'The animation targeting the ::after pesudo-element ' +
                'should be returned last');
  assert_equals(animations[5].effect.target.parentElement, child,
                'This ::after element should be child of child element');
}, 'Test AnimationFilter{ subtree: true } with element that has a child');

test(function(t) {
  var parent = addDiv(t, { 'id': 'parent' });
  var child1 = addDiv(t, { 'id': 'child1' });
  var grandchild1 = addDiv(t, { 'id': 'grandchild1' });
  var grandchild2 = addDiv(t, { 'id': 'grandchild2' });
  var child2 = addDiv(t, { 'id': 'child2' });

  parent.style.animation = 'anim1 100s';
  child1.style.animation = 'anim1 100s';
  grandchild1.style.animation = 'anim1 100s';
  grandchild2.style.animation = 'anim1 100s';
  child2.style.animation = 'anim1 100s';

  parent.appendChild(child1);
  child1.appendChild(grandchild1);
  child1.appendChild(grandchild2);
  parent.appendChild(child2);

  var animations = parent.getAnimations({ subtree: true });
  assert_equals(
    parent.getAnimations({ subtree: true }).length, 5,
                         'Should find all descendants of the element');

  assert_equals(animations[0].effect.target, parent,
                'The animation targeting the parent element ' +
                'should be returned first');

  assert_equals(animations[1].effect.target, child1,
                'The animation targeting the child1 element ' +
                'should be returned second');

  assert_equals(animations[2].effect.target, grandchild1,
                'The animation targeting the grandchild1 element ' +
                'should be returned third');

  assert_equals(animations[3].effect.target, grandchild2,
                'The animation targeting the grandchild2 element ' +
                'should be returned fourth');

  assert_equals(animations[4].effect.target, child2,
                'The animation targeting the child2 element ' +
                'should be returned last');

}, 'Test AnimationFilter{ subtree: true } with element that has many descendant');

done();
</script>
</body>
